package cc.shacocloud.mirage.restful;

import cc.shacocloud.mirage.restful.exception.HttpStatusException;
import cc.shacocloud.mirage.restful.exception.InvalidRequestPathException;
import cc.shacocloud.mirage.restful.exception.ResourceNotFoundException;
import cc.shacocloud.mirage.utils.charSequence.StrUtil;
import cc.shacocloud.mirage.utils.resource.Resource;
import cc.shacocloud.mirage.utils.resource.ResourceUtil;
import io.vertx.core.Future;
import io.vertx.core.MultiMap;
import io.vertx.core.Promise;
import io.vertx.core.http.HttpHeaders;
import io.vertx.core.http.HttpMethod;
import io.vertx.core.http.impl.HttpUtils;
import io.vertx.core.http.impl.MimeMapping;
import io.vertx.core.net.impl.URIDecoder;
import io.vertx.ext.web.impl.LRUCache;
import io.vertx.ext.web.impl.Utils;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.io.IOException;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static cc.shacocloud.mirage.utils.resource.ResourceUtil.CLASSPATH_URL_PREFIX;
import static cc.shacocloud.mirage.utils.resource.ResourceUtil.FILE_URL_PREFIX;
import static io.netty.handler.codec.http.HttpResponseStatus.*;

/**
 * 静态资源处理器
 *
 * @author 思追(shaco)
 */
@Slf4j
public class StaticResourceHandlerImpl implements StaticResourceHandler {
    
    private static final Pattern RANGE = Pattern.compile("^bytes=(\\d+)-(\\d*)$");
    private final ResourceCache cache = new ResourceCache();
    private final String webRoot;
    private long maxAgeSeconds = DEFAULT_MAX_AGE_SECONDS;
    private boolean includeHidden = DEFAULT_INCLUDE_HIDDEN;
    private boolean filesReadOnly = DEFAULT_FILES_READ_ONLY;
    private String indexPage = DEFAULT_INDEX_PAGE;
    private boolean rangeSupport = DEFAULT_RANGE_SUPPORT;
    private boolean sendVaryHeader = DEFAULT_SEND_VARY_HEADER;
    private String defaultContentEncoding = "UTF-8";
    private Set<String> compressedMediaTypes = Collections.emptySet();
    private Set<String> compressedFileSuffixes = Collections.emptySet();
    
    /**
     * 构建基于相对的静态资源绑定器，文件资源在应用程序相对路径和类路径下，类路径相对 {@code pathPattern} 前缀为 {@link StaticResourceHandlerImpl#DEFAULT_WEB_ROOT}
     */
    public StaticResourceHandlerImpl() {
        this(DEFAULT_WEB_ROOT);
    }
    
    /**
     * 构建指定文件系统访问器的静态资源绑定器
     *
     * @param webRoot 相对于 {@code pathPattern} 的路径前缀，支持 {@link ResourceUtil#CLASSPATH_URL_PREFIX} 和 {@link ResourceUtil#FILE_URL_PREFIX} 协议
     */
    public StaticResourceHandlerImpl(String webRoot) {
        
        if (!(StrUtil.startWith(webRoot, CLASSPATH_URL_PREFIX) || StrUtil.startWith(webRoot, FILE_URL_PREFIX))) {
            String protocol = "";
            if (webRoot.contains(":")) {
                protocol = StrUtil.subPre(webRoot, webRoot.indexOf(":") + 1);
            }
            throw new IllegalArgumentException(String.format("不支持的资源协议类型：%s，可用的协议为 %s 或者 %s", protocol,
                    CLASSPATH_URL_PREFIX, FILE_URL_PREFIX));
        }
        
        this.webRoot = StrUtil.removeSuffix(webRoot, "/");
    }
    
    @Override
    public StaticResourceHandlerImpl setFilesReadOnly(boolean readOnly) {
        this.filesReadOnly = readOnly;
        return this;
    }
    
    @Override
    public StaticResourceHandlerImpl setMaxAgeSeconds(long maxAgeSeconds) {
        if (maxAgeSeconds < 0) {
            throw new IllegalArgumentException("maxAgeSeconds 必须大于等于 0");
        }
        this.maxAgeSeconds = maxAgeSeconds;
        return this;
    }
    
    @Override
    public StaticResourceHandlerImpl setMaxCacheSize(int maxCacheSize) {
        cache.setMaxSize(maxCacheSize);
        return this;
    }
    
    @Override
    public StaticResourceHandlerImpl setCachingEnabled(boolean enabled) {
        cache.setEnabled(enabled);
        return this;
    }
    
    @Override
    public StaticResourceHandlerImpl setEnableRangeSupport(boolean enableRangeSupport) {
        this.rangeSupport = enableRangeSupport;
        return this;
    }
    
    @Override
    public StaticResourceHandlerImpl setIncludeHidden(boolean includeHidden) {
        this.includeHidden = includeHidden;
        return this;
    }
    
    @Override
    public StaticResourceHandlerImpl setCacheEntryTimeout(long timeout) {
        cache.setCacheEntryTimeout(timeout);
        return this;
    }
    
    @Override
    public StaticResourceHandlerImpl setIndexPage(@NotNull String indexPage) {
        if (indexPage.charAt(0) == '/') {
            this.indexPage = indexPage.substring(1);
        } else {
            this.indexPage = indexPage;
        }
        return this;
    }
    
    @Override
    public StaticResourceHandlerImpl skipCompressionForMediaTypes(@NotNull Set<String> mediaTypes) {
        this.compressedMediaTypes = new HashSet<>(mediaTypes);
        return this;
    }
    
    @Override
    public StaticResourceHandlerImpl skipCompressionForSuffixes(@NotNull Set<String> fileSuffixes) {
        this.compressedFileSuffixes = new HashSet<>(fileSuffixes);
        return this;
    }
    
    @Override
    public StaticResourceHandlerImpl setSendVaryHeader(boolean sendVaryHeader) {
        this.sendVaryHeader = sendVaryHeader;
        return this;
    }
    
    @Override
    public StaticResourceHandlerImpl setDefaultContentEncoding(@NotNull String contentEncoding) {
        this.defaultContentEncoding = contentEncoding;
        return this;
    }
    
    @Override
    public Future<Resource> findPathResource(@NotNull RoutingContext context) {
        // 解码网址路径
        String uriDecodedPath = URIDecoder.decodeURIComponent(context.normalizedPath(), false);
        // 如果规范化路径为 null，则无法解析
        if (Objects.isNull(uriDecodedPath)) {
            return Future.failedFuture(new InvalidRequestPathException());
        }
        
        // 将所有路径规范化并作为 UNIX 路径处理
        String path = HttpUtils.removeDots(uriDecodedPath.replace('\\', '/'));
        
        String filePath = null;
        
        // 隐藏文件
        if (!includeHidden) {
            filePath = getFile(path, context);
            int idx = filePath.lastIndexOf('/');
            String name = filePath.substring(idx + 1);
            if (name.length() > 0 && name.charAt(0) == '.') {
                return Future.failedFuture(new ResourceNotFoundException("静态资源处理器关闭提供隐藏文件！"));
            }
        }
        
        final CacheEntry entry = cache.get(path);
        
        // 缓存
        if (Objects.nonNull(entry)) {
            if (filesReadOnly || !entry.isOutOfDate()) {
                if (entry.isMissing()) {
                    return Future.failedFuture(new ResourceNotFoundException(String.format("资源路径 %s 不存在！", path)));
                }
                
                Resource resource = entry.getResource();
                
                // 命中 判断文件是否过期
                long lastModified;
                try {
                    lastModified = Utils.secondsFactor(resource.lastModified());
                } catch (IOException e) {
                    return Future.failedFuture(e);
                }
                
                if (Utils.fresh(context.getSource(), lastModified)) {
                    context.response()
                            .setStatusCode(NOT_MODIFIED.code())
                            .end();
                    return Future.succeededFuture();
                }
            }
        }
        
        if (filePath == null) {
            filePath = getFile(path, context);
        }
        
        // 如果是目录则返回默认索引页文件
        filePath = path.endsWith(StrUtil.SLASH) ? filePath + indexPage : filePath;
        
        Resource resource = getResource(filePath);
        
        if (!resource.exists()) {
            if (cache.enabled()) {
                cache.put(path, null);
            }
            
            return Future.failedFuture(new ResourceNotFoundException(String.format("资源路径 %s 不存在！", path)));
        }
        
        if (cache.enabled()) {
            cache.put(path, resource);
        }
        
        long lastModified;
        try {
            lastModified = resource.lastModified();
        } catch (IOException e) {
            return Future.failedFuture(e);
        }
        
        // 判断文件是否刷新
        if (Utils.fresh(context.getSource(), Utils.secondsFactor(lastModified))) {
            context.response().setStatusCode(NOT_MODIFIED.code()).end();
            return Future.succeededFuture();
        }
        
        return Future.succeededFuture(resource);
    }
    
    @Override
    public Future<Void> sendResource(@NotNull HttpResponse response, @NotNull Resource resource) {
        if (response.closed()) return Future.succeededFuture();
        
        HttpRequest request = response.request();
        
        Long offset = null;
        Long end = null;
        MultiMap headers = null;
        
        final String resourcePath = resource.getPath();
        long contentLength;
        try {
            contentLength = resource.contentLength();
        } catch (IOException e) {
            return Future.failedFuture(e);
        }
        
        if (rangeSupport) {
            end = contentLength - 1;
            
            // 检查客户端是否正在发出范围请求
            String range = request.getHeader("Range");
            if (StrUtil.isNotBlank(range)) {
                Matcher m = RANGE.matcher(range);
                if (m.matches()) {
                    try {
                        String part = m.group(1);
                        offset = Long.parseLong(part);
                        if (offset < 0 || offset >= contentLength) {
                            return Future.failedFuture(new IndexOutOfBoundsException());
                        }
                        
                        part = m.group(2);
                        if (part != null && part.length() > 0) {
                            end = Math.min(end, Long.parseLong(part));
                            // 结束偏移量不得小于起始偏移量
                            if (end < offset) {
                                return Future.failedFuture(new IndexOutOfBoundsException());
                            }
                        }
                    } catch (NumberFormatException | IndexOutOfBoundsException e) {
                        response.setHeader(HttpHeaders.CONTENT_RANGE, "bytes */" + contentLength);
                        return Future.failedFuture(new HttpStatusException(REQUESTED_RANGE_NOT_SATISFIABLE));
                    }
                }
            }
            
            // 通知客户端我们支持范围请求
            headers = response.headers();
            headers.set(HttpHeaders.ACCEPT_RANGES, "bytes");
            // 即使对于 HEAD 请求，也发送内容长度
            headers.set(HttpHeaders.CONTENT_LENGTH, Long.toString(end + 1 - (offset == null ? 0 : offset)));
        }
        
        long lastModified;
        try {
            lastModified = resource.lastModified();
        } catch (IOException e) {
            return Future.failedFuture(e);
        }
        
        // 写入缓存请求头
        writeCacheHeaders(request, lastModified);
        
        // HEAD 请求到这里结束
        if (HttpMethod.HEAD.equals(request.method())) {
            response.end();
            return Future.succeededFuture();
        }
        
        long finalOffset = 0, finalLength = Long.MAX_VALUE;
        
        // 范围资源请求
        if (rangeSupport && offset != null) {
            // 必须返回内容范围
            headers.set(HttpHeaders.CONTENT_RANGE, "bytes " + offset + "-" + end + "/" + contentLength);
            // 返回部分响应
            response.setStatusCode(PARTIAL_CONTENT.code());
            
            finalOffset = offset;
            finalLength = end + 1 - offset;
            
            // 根据文件后缀获取内容类型
            String contentType = MimeMapping.getMimeTypeForFilename(resourcePath);
            if (Objects.nonNull(contentType)) {
                if (contentType.startsWith("text")) {
                    response.setHeader(HttpHeaders.CONTENT_TYPE, contentType + ";charset=" + defaultContentEncoding);
                } else {
                    response.setHeader(HttpHeaders.CONTENT_TYPE, contentType);
                }
            }
        }
        // 普通资源请求
        else {
            // 根据文件后缀获取内容类型
            String extension = getFileExtension(resourcePath);
            String contentType = MimeMapping.getMimeTypeForExtension(extension);
            if ((Objects.nonNull(contentType) && compressedMediaTypes.contains(contentType))
                    || compressedFileSuffixes.contains(extension)) {
                response.setHeader(HttpHeaders.CONTENT_ENCODING, HttpHeaders.IDENTITY);
            }
            
            if (Objects.nonNull(contentType)) {
                if (contentType.startsWith("text")) {
                    response.setHeader(HttpHeaders.CONTENT_TYPE, contentType + ";charset=" + defaultContentEncoding);
                } else {
                    response.setHeader(HttpHeaders.CONTENT_TYPE, contentType);
                }
            }
        }
        
        // 写入文件
        // vertx 的 文件系统支持类路径文件，故这边不对类路径文件做处理
        Promise<Void> promise = Promise.promise();
        response.sendFile(resourcePath, finalOffset, finalLength, promise);
        return promise.future();
    }
    
    /**
     * 获取文件拓展名
     */
    @Nullable
    private String getFileExtension(@NotNull String file) {
        int li = file.lastIndexOf('.');
        if (li != -1 && li != file.length() - 1) {
            return file.substring(li + 1);
        } else {
            return null;
        }
    }
    
    /**
     * 创建所有必需的标头，以便缓存服务器或浏览器可以缓存内容
     */
    private void writeCacheHeaders(@NotNull HttpRequest request, long resourceLastModified) {
        MultiMap headers = request.response().headers();
        
        if (cache.enabled()) {
            // 我们使用缓存控制和上次修改
            // 我们不使用 etag 和过期（因为它们做同样的事情 - 冗余）
            Utils.addToMapIfAbsent(headers, HttpHeaders.CACHE_CONTROL, "public, immutable, max-age=" + maxAgeSeconds);
            Utils.addToMapIfAbsent(headers, HttpHeaders.LAST_MODIFIED, Utils.formatRFC1123DateTime(resourceLastModified));
            // 我们发送 vary 标头（用于中间缓存）(假设大多数在使用静态处理程序时会打开压缩）
            if (sendVaryHeader && request.headers().contains(HttpHeaders.ACCEPT_ENCODING)) {
                Utils.addToMapIfAbsent(headers, HttpHeaders.VARY, "accept-encoding");
            }
        }
        
        // 日期标题是必需的
        headers.set("date", Utils.formatRFC1123DateTime(System.currentTimeMillis()));
    }
    
    /**
     * 获取资源对象
     * <p>
     * 如果是以 jar 方法运行，那么类路径下的资源文件将会被拷贝到临时文件目录下
     */
    protected Resource getResource(String filePath) {
        return ResourceUtil.getResource(filePath);
    }
    
    @NotNull
    protected String getFile(@NotNull String path, @NotNull RoutingContext context) {
        return webRoot + Utils.pathOffset(path, context.getSource());
    }
    
    /**
     * 缓存对象条目
     */
    private static final class CacheEntry {
        private final long createDate = System.currentTimeMillis();
        @Getter
        private final Resource resource;
        private final long cacheEntryTimeout;
        
        private CacheEntry(Resource resource, long cacheEntryTimeout) {
            this.resource = resource;
            this.cacheEntryTimeout = cacheEntryTimeout;
        }
        
        boolean isOutOfDate() {
            return System.currentTimeMillis() - createDate > cacheEntryTimeout;
        }
        
        public boolean isMissing() {
            return resource == null;
        }
    }
    
    /**
     * 资源缓存，使用 {@link LRUCache} 缓存算法
     */
    private static class ResourceCache {
        private Map<String, CacheEntry> propsCache;
        private long cacheEntryTimeout = DEFAULT_CACHE_ENTRY_TIMEOUT;
        private int maxCacheSize = DEFAULT_MAX_CACHE_SIZE;
        
        ResourceCache() {
            setEnabled(DEFAULT_CACHING_ENABLED);
        }
        
        boolean enabled() {
            return propsCache != null;
        }
        
        synchronized void setMaxSize(int maxCacheSize) {
            if (maxCacheSize < 1) {
                throw new IllegalArgumentException("maxCacheSize 必须大于等于 1");
            }
            if (this.maxCacheSize != maxCacheSize) {
                this.maxCacheSize = maxCacheSize;
                // 强制创建具有正确大小的缓存
                setEnabled(enabled(), true);
            }
        }
        
        void setEnabled(boolean enable) {
            setEnabled(enable, false);
        }
        
        private synchronized void setEnabled(boolean enable, boolean force) {
            if (force || enable != enabled()) {
                if (propsCache != null) {
                    propsCache.clear();
                }
                if (enable) {
                    propsCache = new LRUCache<>(maxCacheSize);
                } else {
                    propsCache = null;
                }
            }
        }
        
        void setCacheEntryTimeout(long timeout) {
            if (timeout < 1) {
                throw new IllegalArgumentException("timeout 必须大于等于 1");
            }
            this.cacheEntryTimeout = timeout;
        }
        
        void remove(String path) {
            if (propsCache != null) {
                propsCache.remove(path);
            }
        }
        
        @Nullable
        CacheEntry get(String key) {
            if (propsCache != null) {
                return propsCache.get(key);
            }
            
            return null;
        }
        
        void put(String path, Resource resource) {
            if (propsCache != null) {
                CacheEntry now = new CacheEntry(resource, cacheEntryTimeout);
                propsCache.put(path, now);
            }
        }
    }
    
}
